<?
/**
 * Copyright 2007 Melange.
 *
 * This file is part of Melange_Framework
 *
 * PHP-MVC is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * PHP-MVC is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with PHP-MVC; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * @package mvc
 *
 */


/**
 * RequestProcessor class
 *
 * <p><strong>RequestProcessor</strong> contains the processing logic that the
 * {@link ActionServlet} performs as it receives each servlet request from the
 * container. You can customize the request processing behavior by subclassing
 * this class and overriding the method(s) whose behavior you are interested
 * in changing.</p>
 *
 * @category    Melange Framework
 * @package     mvc
 * @copyright   Copyright (c) 2007 Jeroen Simons. All rights reserved
 * @author      Jeroen Simons <jeroen@melange.nl>
 * @link        http://www.melange.nl/
 */
class RequestProcessor {


    /**
     * Logger
     * 
     * @var LogCategory
     */
    private $log;


    // ----------------------------------------------------- Instance Variables


    /**
     * Module config for this processor
     *
     * @var ModuleConfig
     */
    private $moduleConfig;

    
    /**
     * <p>The applicationContext with which we are associated.</p>
     * 
     * @var Context
     */
    protected $applicationContext = null;


    // ----------------------------------------------------------- Constructors


    /**
     * Construct a new Request Processor.
     *
     * @param ModuleConfig $config
     */
    public function __construct(ModuleConfig &$config) {
        $this->log = LoggerManager::getLogger("RequestProcessor");
        $this->moduleConfig = &$config;
    }


    // --------------------------------------------------------- Public Methods


    /**
     * Process the request
     *
     * @param Request $request
     * @param Response $response
     */
    public function process(Request &$request, Response &$response) {

        $path = $this->processPath($request);
        if (is_null($path)) {
            return;
        }

        // Select a Locale for the current user if requested
        $this->processLocale($request, $response);

        // Forward now! continue if forward = null.
        // if(!is_null($forward)) {
        //     $this->processForwardConfig($request, $response, $forward);
        //     return;
        // }

        // Identify the mapping for this request
        $mapping = $this->processMapping($request, $response, $path);

        if (is_null($mapping)) {
            return;
        }

        // Check for any role required to perform this action
        if (!$this->processRoles($request, $response, $mapping)) {
            return;
        }

        // Process any ActionForm bean related to this request
        $form = $this->processActionForm($request, $mapping);

        $this->processPopulate($request, $form, $mapping);

        // Validate any fields of the ActionForm bean, if applicable
        if (!$this->processValidate($request, $response, $form, $mapping)) {
            return;
        }

        // Process a forward or include specified by this mapping
        if (!$this->processForward($request, $response, $mapping)) {
            return;
        }

        // Create or acquire the Action instance to process this request
        $action = $this->processActionCreate($response, $mapping);

        if (is_null($action)) {
            return;
        }

        // Call the Action instance itself
        $forward =
            $this->processActionPerform($request, $response, $action, $form, $mapping);

        // Process the returned ActionForward instance
        $this->processForwardConfig($request, $response, $forward);

    }


    // ------------------------------------------------------ Protected Methods


    /**
     * <p>Identify and return the path component (from the request URI) that
     * we will use to select an <code>ActionMapping</code> with which to
     * dispatch. If no such path can be identified, create an error response
     * and return <code>null</code>.</p>
     *
     * @param Request  The servlet request we are processing
     * @return The path that will be used to select an action mapping.
     */
    protected function processPath(&$request) {

        $path = $request->getPathInfo();

        // Todo: Substract the module-prefix needed ??
        if(strlen($this->moduleConfig->getPrefix()) > 0)
            $path = str_replace("/".$this->moduleConfig->getPrefix()."/", "/", $path);

        if ((!is_null($path)) && (strlen($path) > 0)) {
            return ($path);
        }

        return null;
    }


    /**
     * <p>Return an <code>Action</code> instance that will be used to process
     * the current request, creating a new one if necessary.</p>
     *
     * @param Response The servlet response we are creating
     * @param ActionConfig  The mapping we are using
     * @return An <code>Action</code> instance that will be used to process
     *         the current request.
     */
    protected function processActionCreate(Response &$response, ActionConfig &$mapping) {

        // Acquire the Action instance we will be using (if there is one)
        $className = $mapping->getType();

        if ($this->log->isDebugEnabled()) {
            $this->log->debug(" Looking for Action instance for class " . $className);
        }

        // If there were a mapping property indicating whether
        // an Action were a singleton or not ([true]),
        // could we just instantiate and return a new instance here?
        try {

            // Substract the ClassName of this Action, i.e. EditAction.
            $class = substr(strrchr($className, "/"), 1);

            // Add a slash for no default modules
            $prefix = $this->moduleConfig->getPrefix();
            if(strlen($prefix) > 0)
                $prefix .= "/";

            include_once($className.".php");

            return new $class();

            // Maybe we should propagate this exception
            // instead of returning null.
        } catch (Exception $e) {

            $response->sendError(Response::SC_INTERNAL_SERVER_ERROR,
                "No action instance for path ".mapping.getPath()." could be created");

        }

        return null;
    }


    /**
     * <P>Ask the specified <code>Action</code> instance to handle this
     * request. Return the <code>ActionForward</code> instance (if any)
     * returned by the called <code>Action</code> for further processing.
     * </P>
     *
     * @param request  The servlet request we are processing
     * @param response The servlet response we are creating
     * @param action   The Action instance to be used
     * @param form     The ActionForm instance to pass to this Action
     * @param mapping  The ActionMapping instance to pass to this Action
     * @return The <code>ActionForward</code> instance (if any) returned by
     *         the called <code>Action</code>.
     */
    protected function processActionPerform(Request &$request, Response &$response, Action &$action,
        &$form, ActionConfig &$mapping) {
        try {
            return ($action->execute($mapping, $form, $request, $response));
        } catch (Exception $e) {
        }
        return ($this->processException($request, $response, $e, $form, $mapping));
    }


    /**
     * <p>Automatically select a <code>Locale</code> for the current user, if
     * requested. <strong>NOTE</strong> - configuring Locale selection will
     * trigger the creation of a new <code>HttpSession</code> if
     * necessary.</p>
     *
     * @param request  The servlet request we are processing
     */
    protected function processLocale(Request &$request) {

        // Are we configured to select the Locale automatically?
        if (!$this->moduleConfig->getControllerConfig()->getLocale()) {
            return;
        }

        $session = $request->getSession(true);

        // Has a Locale already been selected?
        if (!is_null($session) && !is_null($session->getAttribute(MVC::LOCALE_KEY))) {
            return;
        }

        // Use the Locale returned by the servlet container (if any)
        $locale = $request->getLocale();
        if (!is_null($locale)) {
            if ($this->log->isDebugEnabled()) {
                $this->log->debug(" Setting user locale '" . $locale . "'");
            }
            $session->setAttribute(MVC::LOCALE_KEY, $locale);
        }
    }


    /**
     * <p>Select the mapping used to process the selection path for this
     * request. If no mapping can be identified, create an error response and
     * return <code>null</code>.</p>
     *
     * @param request  The servlet request we are processing
     * @param response The servlet response we are creating
     * @param path     The portion of the request URI for selecting a mapping
     * @return The mapping used to process the selection path for this
     *         request.
     * @throws IOException if an input/output error occurs
     */
    protected function processMapping(&$request, &$response, $path) {

        // Is there a mapping for this path?
        $mapping = $this->moduleConfig->findActionConfig($path);
        
        // If a mapping is found, put it in the request and return it
        if (!is_null($mapping)) {

            // Store the action instance in view's pageContext
            // Todo: Remove me!
            $this->moduleConfig->getView()->assign("action", $mapping);

            $request->setAttribute(MVC::MAPPING_KEY, $mapping);
            return ($mapping);
        }

        // Locate the mapping for unknown paths (if any)
        $configs = $this->moduleConfig->findActionConfig();

        foreach($configs as $config) {
            if ($config->getUnknown()) {
                $mapping = $config;
                $request->setAttribute(MVC::MAPPING_KEY, $mapping);
                $this->moduleConfig->getView()->assign("action", $mapping);
                return ($mapping);
            }
        }

        if($this->log->isDebugEnabled()) {
            $this->log->debug("No mapping found, 404");
        }

        // No mapping can be found to process this request
        $response->sendError(Response::SC_NOT_FOUND, "Invalid path was requested ".$path);

        return null;
    }


    /**
     * <p>If this action is protected by security roles, make sure that the
     * current user possesses at least one of them.  Return <code>true</code>
     * to continue normal processing, or <code>false</code> if an appropriate
     * response has been created and processing should terminate.</p>
     *
     * @param request  The servlet request we are processing
     * @param response The servlet response we are creating
     * @param mapping  The mapping we are using
     * @return <code>true</code> to continue normal processing;
     *         <code>false</code> if a response has been created.
     * @throws IOException      if an input/output error occurs
     * @throws ServletException if a servlet exception occurs
     */
    protected function processRoles(Request &$request, Response &$response, ActionConfig &$mapping) {

        // Is this action protected by role requirements?
        $roles = $mapping->getRoleNames();

        if (($roles === null) || (count($roles) < 1)) {
            return (true);
        }

        // Check the current user against the list of required roles
        for ($i = 0; $i < count($roles); $i++) {
            if ($request->isUserInRole($roles[$i])) {
                if ($this->log->isDebugEnabled()) {
                    $this->log->debug(" User '" . $request->getRemoteUser()
                        . "' has role '" . $roles[$i] . "', granting access");
                }

                return (true);
            }
        }

        // The current user is not authorized for this action
        if ($this->log->isDebugEnabled()) {
            $this->log->debug(" User '" . $request->getRemoteUser()
                . "' does not have any required role, denying access");
        }

        require_once("mvc/action/AccessDeniedException.php");
        require_once("mvc/action/ActionForm.php");
        $forward = $this->processException($request, $response,
            new AccessDeniedException(), new ActionForm(), $mapping);
            
        // Process the returned ActionForward instance
        $this->processForwardConfig($request, $response, $forward);

//        $response->sendError(Response::SC_FORBIDDEN,
//            "User is not authorized to access action " . $mapping->getPath());

        return (false);
    }


    /**
     * <p>Retrieve and return the <code>ActionForm</code> associated with this
     * mapping, creating and retaining one if necessary. If there is no
     * <code>ActionForm</code> associated with this mapping, return
     * <code>null</code>.</p>
     *
     * @param request  The servlet request we are processing
     * @param response The servlet response we are creating
     * @param mapping  The mapping we are using
     * @return The <code>ActionForm</code> associated with this mapping.
     */
    protected function processActionForm(Request &$request, ActionConfig &$mapping) {

        require_once("mvc/util/RequestUtils.php");

        // Create (if necessary) a form bean to use
        $instance = RequestUtils::createActionForm($request, $mapping, $this->moduleConfig);

        if (is_null($instance)) {
            return (null);
        }

        $attribute = $mapping->getAttribute();
        if($attribute == "unknown") {
            $attribute = addslashes($request->getPathInfo());
        }

        // Store the new instance in the appropriate scope
        if ($this->log->isDebugEnabled()) {
            $this->log->debug(" Storing ActionForm bean instance in scope '"
                . $mapping->getScope() . "' under attribute key '"
                . $attribute . "'");
        }

        if ("request" == $mapping->getScope()) {
            $request->setAttribute($attribute, $instance);
        } else {

            $session = $request->getSession();

            // Look up the form bean configuration information to use
            $name = $mapping->getName();
            $config = $this->moduleConfig->findFormBeanConfig($name);
            $session->setAttribute($attribute, $instance, $config->getType());

        }

        // Store the instance in view's pageContext
        $this->moduleConfig->getView()->assign($mapping->getName(), $instance);

        return ($instance);
    }


    /**
     * <p>Process a forward requested by this mapping (if any). Return
     * <code>true</code> if standard processing should continue, or
     * <code>false</code> if we have already handled this request.</p>
     *
     * @param request  The servlet request we are processing
     * @param response The servlet response we are creating
     * @param mapping  The ActionMapping we are using
     * @return <code>true</code> to continue normal processing;
     *         <code>false</code> if a response has been created.
     * @throws IOException      if an input/output error occurs
     * @throws ServletException if a servlet exception occurs
     */
    protected function processForward(Request &$request,
        Response &$response, ActionConfig &$mapping) {

        // Are we going to process this request?
        $forward = $mapping->getForward();

        if ($forward === null) {
            return true;
        }

        $this->internalModuleRelativeForward($forward, $request, $response);

        return (false);
    }


    /**
     * <p>Populate the properties of the specified <code>ActionForm</code>
     * instance from the request parameters included with this request.  In
     * addition, request attribute <code>MVC.CANCEL_KEY</code> will be set
     * if the request was submitted with a button created by
     * <code>CancelTag</code>.</p>
     *
     * @param request  The servlet request we are processing
     * @param form     The ActionForm instance we are populating
     * @param mapping  The ActionMapping we are using
     * @throws ServletException if thrown by RequestUtils.populate()
     */
    protected function processPopulate(Request &$request,
        ActionForm &$form = null, ActionConfig &$mapping) {

        if (is_null($form)) {
            return;
        }

        // Populate the bean properties of this ActionForm instance
        if ($this->log->isDebugEnabled()) {
            $this->log->debug(" Populating bean properties from this request");
        }

        $form->reset($mapping, $request);

        RequestUtils::populate($form, $request);

        // Set the cancellation request attribute if appropriate
        if (!is_null($request->getParameter(MVC::CANCEL_KEY))) {
            $request->setAttribute(MVC::CANCEL_KEY, true);
        }

        // Todo: Remove this somehow. Also save form values after Action::execute() ?
        if ("session" == $mapping->getScope()) {
            $session = $request->getSession();
            // Look up the form bean configuration information to use
            $name = $mapping->getName();
            $config = $this->moduleConfig->findFormBeanConfig($name);

            $attribute = $mapping->getAttribute();
            if($attribute == "unknown") {
                $attribute = addslashes($request->getPathInfo());
            }
            $session->setAttribute($attribute, $form, $config->getType());
        }

    }


    /**
     * <p>If this request was not cancelled, and the request's {@link
     * ActionMapping} has not disabled validation, call the
     * <code>validate</code> method of the specified {@link ActionForm}, and
     * forward to the input path if there were any errors. Return
     * <code>true</code> if we should continue processing, or
     * <code>false</code> if we have already forwarded control back to the
     * input form.</p>
     *
     * @param request  The servlet request we are processing
     * @param response The servlet response we are creating
     * @param form     The ActionForm instance we are populating
     * @param mapping  The ActionMapping we are using
     * @return <code>true</code> to continue normal processing;
     *         <code>false</code> if a response has been created.
     * @throws IOException      if an input/output error occurs
     * @throws ServletException if a servlet exception occurs
     * @throws InvalidCancelException if a cancellation is attempted
     *         without the proper action configuration.
     */
    protected function processValidate(Request &$request, Response &$response,
        &$form, ActionConfig &$mapping) {

        if (is_null($form)) {
            return (true);
        }

        // Has validation been turned off for this mapping?
        if (!$mapping->getValidate()) {
            return (true);
        }

        // Was this request cancelled? If it has been, the mapping also
        // needs to state whether the cancellation is permissable; otherwise
        // the cancellation is considered to be a symptom of a programmer
        // error or a spoof.
        if (!is_null($request->getAttribute(MVC::CANCEL_KEY))) {
            if ($mapping->getCancellable()) {
                if ($this->log->isDebugEnabled()) {
                    $this->log->debug(" Cancelled transaction, skipping validation");
                }
                return (true);
            } else {
                $request->removeAttribute(MVC::CANCEL_KEY);
                throw new Exception();
            }
        }

        // Call the form bean's validation method
        if ($this->log->isDebugEnabled()) {
            $this->log->debug(" Validating input form properties");
        }

        $errors = $form->validate($mapping, $request);

        if ((is_null($errors)) || $errors->isEmpty()) {
            if ($this->log->isDebugEnabled()) {
                $this->log->debug("  No errors detected, accepting input");
            }
            return (true);
        }

        // Was an input path (or forward) specified for this mapping?
        $input = $mapping->getInput();

        if (is_null($input)) {
            $this->log->error("  Validation failed but no input form available");

            $response->sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                "No input attribute for mapping path ".$mapping->getPath());

            return (false);
        }

        // Save our error messages and return to the input form if possible
        if ($this->log->isDebugEnabled()) {
            $this->log->debug(" Validation failed, returning to '" . $input . "'");
        }

        $request->setAttribute(MVC::ERROR_KEY, $errors);

        $this->internalModuleRelativeForward($input, $request, $response);

        return (false);
    }


    /**
     * <p>Ask our exception handler to handle the exception. Return the
     * <code>ActionForward</code> instance (if any) returned by the called
     * <code>ExceptionHandler</code>.</p>
     *
     * @param request   The servlet request we are processing
     * @param response  The servlet response we are processing
     * @param exception The exception being handled
     * @param form      The ActionForm we are processing
     * @param mapping   The ActionMapping we are using
     * @return The <code>ActionForward</code> instance (if any) returned by
     *         the called <code>ExceptionHandler</code>.
     * @throws IOException      if an input/output error occurs
     * @throws ServletException if a servlet exception occurs
     */
    protected function processException(Request &$request,
        Response &$response, Exception &$exception, &$form,
        ActionConfig &$mapping) {

        // Is there a defined handler for this exception?
        $config = $mapping->findException($exception);

        if ($config === null) {

            $this->log->warn("Unhandled Exception thrown: " .
                    get_class($exception));

            if ($exception instanceof Exception)
                throw $exception;
        }

        // Use the configured exception handling
        try {

            $className = $config->getHandler();

            // Substract the ClassName of this Action, i.e. EditAction.
            $class = substr(strrchr($className, "/"), 1);

            // Add a slash for no default modules
            $prefix = $this->moduleConfig->getPrefix();
            if(strlen($prefix) > 0)
                $prefix .= "/";

            include_once($className.".php");

            $handler = new $class();
            return ($handler->execute($exception, $config, $mapping, $form, $request,
                $response));

        } catch (Exception $e) {
            throw $e;
        }

        return null;
    }


    /**
     * <p>Forward or redirect to the specified destination, by the specified
     * mechanism.  This method uses a <code>ForwardConfig</code> object
     * instead an <code>ActionForward</code>.</p>
     *
     * @param request  The servlet request we are processing
     * @param response The servlet response we are creating
     * @param forward  The ForwardConfig controlling where we go next
     * @throws IOException      if an input/output error occurs
     * @throws ServletException if a servlet exception occurs
     */
    protected function processForwardConfig(Request &$request, Response &$response, &$forward) {

        if (is_null($forward)) {
            return;
        }

        if ($this->log->isDebugEnabled()) {
            $this->log->debug("processForwardConfig(".$forward->getPath().")");
        }

        $forwardPath = $forward->getPath();

        // paths not starting with / should be passed through without any
        // processing (ie. they're absolute)
        if (strpos($forwardPath, "/") === 0) {
            // get module relative uri
            $uri = RequestUtils::forwardURL($forward, $this->moduleConfig, $forward->getRedirect());
        } else {
            $uri = $forwardPath;
        }

        if ($this->log->isDebugEnabled()) {
            $this->log->debug("uri(".$uri.")");
        }


        if ($forward->getRedirect()) {

            // only prepend context path for relative uri
            // Todo: Somehow this differs from Java. A Base Path is always needed here. ?
            if (strpos($uri, "/") == 0) {
                $uri = substr($uri, 1);
            }
            $uri = $request->getContextPath()."/".$uri;

            $response->sendRedirect($response->encodeRedirectURL($uri));

        } else {
            $this->doForward($uri, $request, $response);
        }
    }


    /**
     * <p>Do a module relative forward to specified URI using request
     * dispatcher. URI is relative to the current module. The real URI is
     * compute by prefixing the module name.</p> <p>This method is used
     * internally and is not part of the public API. It is advised to not use
     * it in subclasses. </p>
     *
     * @param uri      Module-relative URI to forward to
     * @param request  Current page request
     * @param response Current page response
     * @throws IOException      if an input/output error occurs
     * @throws ServletException if a servlet exception occurs
     * @since Struts 1.1
     */
    protected function internalModuleRelativeForward($uri, Request &$request, Response &$response) {

        // paths not starting with / should be passed through without any
        // processing (ie. they're absolute)
        if (strpos($uri, "/") === 0) {
            // get module relative uri
            $uri = RequestUtils::forwardURL(new ForwardConfig(array("path"=>$uri)), $this->moduleConfig);
        }
        
        // Delegate the processing of this request
        // :FIXME: - exception handling?
        if ($this->log->isDebugEnabled()) {
            $this->log->debug(" Delegating via forward to '" . $uri . "'");
        }

        $this->doForward($uri, $request, $response);
    }


    /**
     * <p>Do a forward to specified URI using a <code>Zend View</code>.
     * This method is used by all internal method needing to do a
     * forward.</p>
     *
     * @param uri      Context-relative URI to forward to
     * @param response Current page response
     * @throws IOException      if an input/output error occurs
     * @throws ServletException if a servlet exception occurs
     * @since Struts 1.1
     */
    protected function doForward($uri, Request &$request, Response &$response) {

        $this->moduleConfig->getView()->request  = $request;
        $this->moduleConfig->getView()->response = $response;
        $this->moduleConfig->getView()->session  = $request->getSession(false);

        $response->resetBuffer();

        try {

            $this->moduleConfig->getView()->render($uri);

        } catch(Exception $e) {

            $mapping = $request->getAttribute(MVC::MAPPING_KEY);
            $formName = !is_null($mapping) ? $mapping->getName() : null;
            $form = !is_null($formName) ?
                $this->moduleConfig->getView()->$formName : null;
            $forward = $this->processException($request, $response, $e, $form, $mapping);
            if($uri != $forward->getPath())
                $this->processForwardConfig($request, $response, $forward);

        }
    }


    /**
     * <p>Return the <code>ApplicationContext</code> for the web application in
     * which we are running.</p>
     *
     * @return Context The <code>ApplicationContext</code> for the web application.
     */
    protected function getApplicationContext() {
        return ($this->applicationContext);
    }

}
?>
